fix(swift-sdk): fixed mempool tx categorization after restart#3777
fix(swift-sdk): fixed mempool tx categorization after restart#3777ZocoLini wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThis PR updates UTXO spend-state persistence in ChangesTransaction spend-state confirmation gating
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Review complete (commit e3c20ea) |
e3c20ea to
f9c28f8
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 660-675: The current logic unconditionally derives txo.isSpent
from the incoming spendingTransaction and will clear a previously confirmed
spend when a later unconfirmed conflicting mempool sighting arrives; change the
update so that if the existing txo.spendingTransaction is confirmed (use
Self.spendIsInBlock on txo.spendingTransaction) and the incoming
spendingTransaction is unconfirmed (expectedIsSpent == false) and has a
different txid than txo.spendingTransaction, then skip changing txo.isSpent and
do not overwrite the canonical spending link; only allow the update when the
incoming spender is confirmed, or when the incoming txid equals the existing one
(same-tx downgrade/upgrade) or when an explicit reorg downgrade path is taken.
Apply the same guard to the other similar blocks around the indicated regions
(lines ~843-872 and ~901-922) referencing the same symbols: txo,
spendingTransaction, spendingTxid, inputIndex, and Self.spendIsInBlock.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: aa0944c8-4597-4813-ad8c-c8517d9f145c
📒 Files selected for processing (1)
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
|
✅ DashSDKFFI.xcframework built for this PR.
SwiftPM (host the zip at a stable URL, then use): .binaryTarget(
name: "DashSDKFFI",
url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
checksum: "b7487f6f0a59d47773eb5a9b4539d6bcce339d0b0063d87caff4b6c4a0498437"
)Xcode manual integration:
|
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
The PR correctly identifies and partially fixes the mempool self-send restart-misclassification bug. However, spendIsInBlock is too strict: it excludes instantSend, which this codebase already treats as confirmed (CoreBalance.confirmed doc string says "in a block or InstantSend-locked"). Combined with loadWalletList's isSpent == false filter, this regresses InstantSend spends across a restart, re-feeding the spent UTXO to Rust as unspent. A second, narrower concern: markUtxoSpent now flips isSpent back to false whenever the spending tx can't be resolved, including the empty/zero spending_txid guard path, which previously was an authoritative spend signal.
🔴 1 blocking | 🟡 1 suggestion(s) | 💬 1 nitpick(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- [BLOCKING] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:639-641: InstantSend-locked spends regress to unspent across a restart
`TransactionContextType` ordering is `mempool=0, instantSend=1, inBlock=2, inChainLockedBlock=3`, so `tx.context >= inBlock.rawValue` excludes `instantSend`. The stated reason for the new gate — "reversible by RBF or mempool eviction" — does not apply to InstantSend: by Dash design it cannot be reorged out and cannot be replaced. Treating it the same as mempool produces two concrete regressions:
1. While the spending tx is `instantSend` but not yet in a block, the input TXO keeps `isSpent == false`, so the wallet UI/queries that read SwiftData see the row as still spendable.
2. Across a restart in that window, `loadWalletList` (line 2948) filters TXOs by `isSpent == false` and hands the row back to Rust as a live UTXO. Nothing in the restore path rebuilds ordinary spending transactions into Rust's in-memory graph, so the spent input is silently re-credited until a later block update arrives, which can overstate spendable funds and enable a duplicate-spend attempt.
This contradicts the project's own contract — `ManagedCoreWallet.CoreBalance.confirmed`'s doc literally says "Confirmed balance (in a block or InstantSend-locked)" — so the persistence layer should follow the same definition.
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:915-921: `markUtxoSpent` can now clear a previously-set `isSpent`
Before this PR, `markUtxoSpent` was an authoritative spend signal: once the TXO was located, `isSpent` was set to `true` unconditionally. After this change the tail-write is `txo.isSpent = spendingTx.map(Self.spendIsInBlock) ?? false`, which can flip `isSpent` from `true` back to `false` in three paths:
1. The guard at line 902 leaves `spendingTx == nil` when the FFI emits an empty/all-zero `spending_txid`. The fact that this guard exists implies Rust can emit that case — and the legacy semantic there was "mark as spent anyway".
2. The spending tx hasn't been persisted yet this flush. The comment promises self-healing on the next flush, but until then the TXO observably toggles back to spendable.
3. A TXO already confirmed-spent in an earlier flush is re-emitted here with the spending tx's record evicted from this `backgroundContext` — `isSpent` goes `true → false`.
The PR's stated intent ("don't permanently flip `isSpent` from a mempool-only sighting") only requires the negative case for mempool. An upgrade-only rule — never clear `isSpent` here, only set `false → true` when the spending tx is confirmed — preserves the fix while keeping the existing authoritative-signal semantics. Same shape should be applied at the deferred-pending drain (line 872) for consistency.
|
I will be taking a look into pasta claw complains to be able to preperly answer them |
f9c28f8 to
95b219d
Compare
After a mid-flight restart between mempool sighting and block confirmation, a self-send tx was misclassified as Incoming with netAmount = +sum_of_outputs. The persister flipped PersistentTxo.isSpent = true on the mempool sighting, and loadWalletList filters by isSpent == false, so the input TXO was dropped from the restored set — the catch-up classifier on the next launch saw an "unknown" input and emitted Incoming.
Fix: gate the isSpent write on the spending tx being in a block (context >= inBlock). Mempool sightings still link the spendingTransaction relationship but keep the row in the unspent set. The next upsert with a confirmed context flips isSpent then.
Checklist:
For repository code-owners and collaborators only
Summary by CodeRabbit